local Stack = require 'stack'


local Minefield = {}
local mt = {__index=Minefield}


--###
--#O#   # - these guys
--###   |
--      |
--      V
local surroundings = {}
for x = -1, 1 do
    for y = -1, 1 do
        if x ~= 0 or y ~= 0 then
            table.insert(surroundings, {x, y})
        end
    end
end



-- Creates new minefield $width wide and $height high
-- and with $amount mines
-- amount < width*height
function Minefield.new(width, height, amount)
    if not amount < width * height then
        error(string.format(
            "Can not create new minefield with dimensions %d x %d: impossible to fit %d mines!",
            amount, width, height))
    end

    local mf = setmetatable({isUntouched=true, gameOver=false}, mt)
    mf.width = width
    mf.height = height

    -- prepare field for mine filling
    mf.field = {}

    for x = 1, width do
        table.insert(mf.field, {})
        
        for y = 1, height do
            mf.field[x][y] = {isMine=false, isOpen=false, neighborMineCount=0}
        end
    end

    -- fill with mines
    local mines_left = amount
    mf.amount = 0

    --[[
    local freeCells = mf:_filterField(  
        function(c)
            return not c.isMine
        end)
    --]]

    while mines_left > 0 do

        repeat
            x = math.random(1, mf.width)
            y = math.random(1, mf.height)
        until mf.field[x][y].isMine == false

        mf.field[x][y].isMine = true

        mines_left = mines_left - 1
        mf.amount = mf.amount + 1
    end

    -- set neighbor numbers
    mf:_calculateMineNumbers()

    return mf
end


function Minefield:_isInBounds(x, y)
    if 1 <= x and x <= self.width and
        1 <= y and y <= self.height then
        return true
    else
        return false
    end
end


function Minefield:isMine(x, y)
    if self:_isInBounds(x, y) and self.field[x][y].isMine == true then
        return true
    else
        return false
    end
end


function Minefield:isNumber(x, y)
    if self:_isInBounds(x, y) and self.field[x][y].neighborMineCount > 0 then
        return true
    else
        return false
    end
end


function Minefield:isOpen(x, y)
    if self:_isInBounds(x, y) and self[x][y].isOpen == true then
        return true
    else
        return false
    end
end


-- returns all cells coords filtered by function bool f(cell).
function Minefield:_filterField(f)
    local coords = {}
    for x = 1, self.width do
        for y = 1, self.height do
            if f(self.field[x][y]) then
                table.insert(coords, {x, y})
            end
        end
    end

    return coords
end

-- returns cells coords around (ox, oy) filtered by function bool f(cell).
function Minefield:_filterSurroundings(f, ox, oy)
    local coords = {}
    for _, delta in surroundings do
        local dx, dy = unpack(delta)
        local x, y = ox+dx, oy+dy
        if self:_isInBounds(x, y) and f(self.field[x][y]) then
            table.insert(coords, {x, y})
        end
    end

    return coords
end


-- returns amount of mines around given cell coords 
function Minefield:_getMineCount(x, y)
    if Minefield:_isInBounds(x,y) then
        local neighbors = self:_filterField(
            function(c) 
                return c.isMine 
            end, 
            x, y)

        return #neighbors
    end
end


-- calculates those numbers
function Minefield:_calculateMineNumbers()
    for x = 1, self.width do
        for y = 1, self.height do
            self.field[x][y].neighborMineCount = self:_getMineCount(x, y)
        end
    end
end


-- when at first strike player hits a mine,
-- it needs to be relocated
function Minefield:_relocateMine(x, y)
    if not self:isMine(x, y) then
        error(string.format("Unable to relocate mine at (%d,%d): mine not found!", x, y))
    end

    local freeCells = self:_filterField(
        function(c)
            return not c.isMine
        end)

    local nx, ny = unpack(_choice(freeCells))
    self.field[x][y].isMine = false
    self.field[nx][ny].isMine = true
    self:_calculateMineNumbers()
end


local function _choice(t)
    if #t > 0 then
        return t[random.random(1, #t)]
    end
end


-- returns all cells around (x,y), that are free
-- and not open
-- TOCHANGE
function Minefield:_availableCells(x, y)
    return self:_filterSurroundings(
        function(c)
            return not c.isMine or c.isOpen
        end,
        x, y)
end

-- TODO
function Minefield:openCells(x, y)
    if self:_isInBounds(x, y) then

        if self:isMine(x, y) then
            if self.isUntouched then
                self:_relocateMine(x, y)
            else
                self.gameOver = true
            end
        end

        local stack = Stack.new()

        local running = true
        local first = true
        while running do
            if self:isNumber(x, y) then
                self.field[x][y].isOpen = true

                if first then
                    first = false
                    stack:push({x,y})
                    x, y = unpack(_choice(self:_availableCells(x, y)))
                else
                    x, y = unpack(stack:pop())
                end
            end

            if not stack:isEmpty() then
                x, y = unpack(_choice(self:_availableCells(x, y)))
            else
                running = false
            end
        end
    end
end


return Minefield